/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package ru.rantipin.media;

import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.media.Buffer;
import javax.media.Codec;
import javax.media.ConfigureCompleteEvent;
import javax.media.ControllerClosedEvent;
import javax.media.ControllerEvent;
import javax.media.ControllerListener;
import javax.media.Format;
import javax.media.Manager;
import javax.media.MediaLocator;
import javax.media.NoDataSourceException;
import javax.media.PrefetchCompleteEvent;
import javax.media.Processor;
import javax.media.RealizeCompleteEvent;
import javax.media.ResourceUnavailableEvent;
import javax.media.ResourceUnavailableException;
import javax.media.StartEvent;
import javax.media.UnsupportedPlugInException;
import javax.media.control.FormatControl;
import javax.media.control.TrackControl;
import javax.media.format.RGBFormat;
import javax.media.format.VideoFormat;
import javax.media.protocol.CaptureDevice;
import javax.media.protocol.DataSource;
import ru.rantipin.utilities.ReflectionMediaData;

/**
 *
 * @author rantipin
 */
public class MediaProcessor implements ControllerListener{

    public final static int BYT_PER_PIXEL = 3;
    
    public static enum MediaBounds {
        LEFT, RIGHT, TOP, BOTTOM
    }

    public MediaProcessor(ReflectionMediaData reflection) {
        this.reflection = reflection;
    }
    
    private DataSource createDataSource4CaptureDevice(String deviceName, RGBFormat rgb) throws MediaProcessorException {
        
        DataSource ds = null;
        
        try {
            int index = MediaHelper.getInstance().getDeviceIndex(deviceName);
            if (index == -1) {
                String message = "Can't find device information by device name.";
                Logger.getLogger(MediaProcessor.class.getName()).log(Level.SEVERE, message);
                throw new MediaProcessorException(message);
            }
            
            MediaLocator ml = new MediaLocator(deviceName);

            ds = Manager.createDataSource(ml);
            ds.connect();

            if (ds instanceof CaptureDevice) {
                CaptureDevice capture = (CaptureDevice) ds;
                FormatControl[] fcs = capture.getFormatControls();
                FormatControl fc = fcs[index];
                fc.setFormat(rgb);
            } else {
                String message = "DataSource is not valid capture device.";
                Logger.getLogger(MediaProcessor.class.getName()).log(Level.SEVERE, message);
                throw new MediaProcessorException(message);
            } 

        } catch (Exception ex) {
            String message = "Can't create data source.";
            Logger.getLogger(MediaProcessor.class.getName()).log(Level.SEVERE, message, ex);
            throw new MediaProcessorException(message, ex);
        }
        
        return ds;
    }
    
    private void prepareProcessor(DataSource ds) throws MediaProcessorException {
        try {
            processor = Manager.createProcessor(ds);
        } catch (Exception ex) {
            String message = "Failed to create a processor from the given data source.";
            Logger.getLogger(MediaProcessor.class.getName()).log(Level.SEVERE, message, ex);
            throw new MediaProcessorException(message, ex);
        }

        processor.addControllerListener(this);

        processor.configure();
        if (!waitForState(processor.Configured)) {
            String message = "Failed to configure the processor.";
            Logger.getLogger(MediaProcessor.class.getName()).log(Level.SEVERE, message);
            throw new MediaProcessorException(message);
        }
        
        processor.setContentDescriptor(null);
        
        TrackControl tc[] = processor.getTrackControls();
        if (tc == null) {
            String message = "Failed to obtain track controls from the processor.";
            Logger.getLogger(MediaProcessor.class.getName()).log(Level.SEVERE, message);
        }
        
        TrackControl videoTrack = null;
        for (int i = 0; i < tc.length; i++) {
            if (tc[i].getFormat() instanceof VideoFormat) {
                videoTrack = tc[i];
                break;
            }
        }
        
        if (videoTrack == null) {
            String message = "The input media does not contain a video track.";
            Logger.getLogger(MediaProcessor.class.getName()).log(Level.SEVERE, message);
        }
        
        try {
            postAccessCodec = new PostAccessCodec();
            Codec codec[] = { postAccessCodec };
            videoTrack.setCodecChain(codec);
        } catch (UnsupportedPlugInException ex) {
            String message = "The process does not support effects.";
            Logger.getLogger(MediaProcessor.class.getName()).log(Level.SEVERE, message, ex);
            throw new MediaProcessorException(message, ex);
        }

        processor.prefetch();
        if (!waitForState(processor.Prefetched)) {
            String message = "Failed to realize the processor.";
            Logger.getLogger(MediaProcessor.class.getName()).log(Level.SEVERE, message);
        }
    }
    
    boolean waitForState(int state) {
        synchronized (waitSync) {
            try {
                while (processor.getState() != state && stateTransitionOK) {
                    waitSync.wait();
                } 
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
                Logger.getLogger(MediaProcessor.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        return stateTransitionOK;
    }
    
    public void controllerUpdate(ControllerEvent event) {
        if (event instanceof ConfigureCompleteEvent ||
            event instanceof RealizeCompleteEvent ||
            event instanceof PrefetchCompleteEvent ||
            event instanceof StartEvent) {
            synchronized (waitSync) {
                stateTransitionOK = true;
                waitSync.notifyAll();
            }
        } else if (event instanceof ResourceUnavailableEvent) {
            synchronized (waitSync) {
                stateTransitionOK = false;
                waitSync.notifyAll();
            }
        } else if (event instanceof ControllerClosedEvent) {
            synchronized (waitSync) {
                closeDataSource = true;
                waitSync.notifyAll();
            }
        }
    }
    
    public void initialize(String deviceName, RGBFormat rgb) throws MediaProcessorException {
        DataSource ds = createDataSource4CaptureDevice(deviceName, rgb);
        prepareProcessor(ds);
    }
    
    public void initialize(String file) throws MediaProcessorException {
        String message = "Can't create data source from media locator.";
        try {
            MediaLocator ml = new MediaLocator(file);
            prepareProcessor(Manager.createDataSource(ml));
        } catch (IOException ex) {
            Logger.getLogger(MediaProcessor.class.getName()).log(Level.SEVERE, message, ex);
            throw new MediaProcessorException(message, ex);
        } catch (NoDataSourceException ex) {
            Logger.getLogger(MediaProcessor.class.getName()).log(Level.SEVERE, message, ex);
            throw new MediaProcessorException(message, ex);
        }
    }
    
    public void startProcessor() {
        processor.start();
        if (!waitForState(processor.Started)) {
            String message = "Failed to started the processor";
            Logger.getLogger(MediaProcessor.class.getName()).log(Level.SEVERE, message);
        }
        closeDataSource = false;
    }
    
    public void stopProcessor() {
        synchronized(waitSync) {
            if (processor != null) {
                processor.close();
            } else {
                Logger.getLogger(MediaProcessor.class.getName()).log(Level.INFO, "Media processor is not create yet");
            }
            try {
                while (!closeDataSource) {
                    waitSync.wait(10000); 
                }
            } catch (InterruptedException ex) {
                Logger.getLogger(MediaProcessor.class.getName()).log(Level.SEVERE, "Can't stop media data source correctly", ex);
            }
        }
    }
    
    public Component getVisualComponent() throws MediaProcessorException {
        Component comp = processor.getVisualComponent();
        if (comp == null) {
            String message = "Can't resive visual component";
            Logger.getLogger(MediaProcessor.class.getName()).log(Level.SEVERE, message);
            throw new MediaProcessorException(message);
        }
        
        comp.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
        comp.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                super.mouseClicked(e);
                
                switch(mediaBounds) {
                    case LEFT:
                        postAccessCodec.leftBound = e.getX() >= postAccessCodec.rightBound ? postAccessCodec.rightBound: e.getX();
                        break;
                    case RIGHT:
                        postAccessCodec.rightBound = e.getX() <= postAccessCodec.leftBound ? postAccessCodec.leftBound: e.getX();
                        break;
                    case TOP:
                        postAccessCodec.topBound = e.getY() >= postAccessCodec.bottomBound ? postAccessCodec.bottomBound: e.getY();
                        break;
                    case BOTTOM:
                        postAccessCodec.bottomBound = e.getY() <= postAccessCodec.topBound ? postAccessCodec.topBound: e.getY();
                        break;
                }
            }
        
        });
        
        return comp;
    }
    
    public Component getControlComponent() throws MediaProcessorException {
        Component comp = processor.getControlPanelComponent();
        if (comp == null) {
            String message = "Can't resive control panel component.";
            Logger.getLogger(MediaProcessor.class.getName()).log(Level.SEVERE, message);
            throw new MediaProcessorException(message);
        }
        
        return comp;
    }
    
    private class PostAccessCodec implements Codec {
        
        public Format[] getSupportedInputFormats() {
            return inputFormats;
        }

        public Format[] getSupportedOutputFormats(Format input) {
            if(input == null) {
                return outputFormats;
            } else if(matches(input,inputFormats) != null) {
                return new Format[]{outputFormats[0].intersects(input)};
            } else {
                return new Format[0];
            }
        }

        public Format setInputFormat(Format input) {
            inputFormat = input;
            return input;
        }

        public Format setOutputFormat(Format output) {
            if(output==null || matches(output,outputFormats) == null) {
                    return null;
            }

            RGBFormat incoming = (RGBFormat)output;

            Dimension size = incoming.getSize();
            int maxDataLength = incoming.getMaxDataLength();
            int lineSt = incoming.getLineStride();
            float frameRate = incoming.getFrameRate();
            int flipped = incoming.getFlipped();

            if(size==null) {
                return null;
            }
            if (maxDataLength < size.width * size.height * 3) {
                    maxDataLength = size.width * size.height * 3;
            }
            if (lineSt < size.width * 3) {
                    lineSt = size.width * 3;
            }
            flipped = Format.FALSE;

            outputFormat = outputFormats[0].intersects(new RGBFormat(size,maxDataLength,null,
                    frameRate,Format.NOT_SPECIFIED,Format.NOT_SPECIFIED,Format.NOT_SPECIFIED,
                    Format.NOT_SPECIFIED,Format.NOT_SPECIFIED,lineSt,
                    Format.NOT_SPECIFIED,Format.NOT_SPECIFIED));

            
            if (outputFormat instanceof  RGBFormat) {
                
                RGBFormat rgb = (RGBFormat)outputFormat;
                
                leftBound = 1;
                rightBound = size.width - 1;
                topBound = 1;
                bottomBound = size.height - 1;
                
                width = rgb.getSize().width;
                height = rgb.getSize().height;
                lineStride = rgb.getLineStride();
                dataLenght = rgb.getMaxDataLength();
            }
            
            return outputFormat;
        }
        
        public int process(Buffer in, Buffer out) {
            
            byte[] inData = (byte[])in.getData();
            
            in.setData(out.getData());
            
            out.setLength(in.getLength());
            out.setFormat(in.getFormat());
            out.setFlags(in.getFlags());
            out.setTimeStamp(in.getTimeStamp());
            out.setSequenceNumber(in.getSequenceNumber());

            if(in.isDiscard() || inData==null) {
                Logger.getLogger(PostAccessCodec.class.getName()).log(Level.SEVERE, "Attention, discard input frame data.");
                return BUFFER_PROCESSED_FAILED;
            }
            
            if (useblackwhite) {
                blackwithe(inData);
            }
            
            reflection.processMediaData(inData, lineStride, leftBound, rightBound, topBound, bottomBound);
            
            paintLine(inData);
            
            out.setData(inData);
            
            return BUFFER_PROCESSED_OK;

        }

        private void blackwithe(byte[] inData) {
            int index;
            byte value;
            
            for(int i = 0; i < dataLenght / BYT_PER_PIXEL; i++) {
                index = i * BYT_PER_PIXEL;
                value = (byte)((inData[index] & 0xFF) * 0.11f + (inData[index + 1] & 0xFF) * 0.59f + (inData[index + 2] & 0xFF) * 0.3f);
                inData[index] = inData[index + 1] = inData[index + 2] = value;
            }
        }
        
        private void paintLine(byte[] inData) {
            
            for(int i = 0; i < width; i++) {
                int topPixel = (height - topBound) * lineStride + BYT_PER_PIXEL * i;
                
                inData[topPixel    ] = (byte)0;
                inData[topPixel + 1] = (byte)0;
                inData[topPixel + 2] = (byte)255;
                
                int bottomPixel = (height - bottomBound) * lineStride + BYT_PER_PIXEL * i;
                
                inData[bottomPixel    ] = (byte)0;
                inData[bottomPixel + 1] = (byte)0;
                inData[bottomPixel + 2] = (byte)255;
            }
            
            for (int i = 0; i < height; i++) {
                int leftPixel = leftBound * BYT_PER_PIXEL + lineStride * i;
                
                inData[leftPixel    ] = (byte)0;
                inData[leftPixel + 1] = (byte)0;
                inData[leftPixel + 2] = (byte)255;
                
                int rightPixel = rightBound * BYT_PER_PIXEL + lineStride * i; 
                
                inData[rightPixel    ] = (byte)0;
                inData[rightPixel + 1] = (byte)0;
                inData[rightPixel + 2] = (byte)255;
            }
        }
        
        public String getName() {
            return "PostAccessCodec";
        }

        public void open() throws ResourceUnavailableException {
        }

        public void close() {
        }

        public void reset() {
        }

        public Object[] getControls() {
            return new Object[0];
        }

        public Object getControl(String arg0) {
            return null;
        }
        
        Format matches(Format in, Format outs[]) {
            for(int i = 0; i < outs.length; i++){
                if (in.matches(outs[i])) {
                    return outs[i];
                }
            }	
            return null;
        }
        
        
        int width, height, lineStride, dataLenght;
        
        volatile int leftBound, rightBound, topBound, bottomBound;
        
        private Format inputFormat = null;
        private Format outputFormat = null;

        private Format []inputFormats = new Format[]{new RGBFormat(null,Format.NOT_SPECIFIED,Format.byteArray,
				Format.NOT_SPECIFIED,24,3, 2, 1,3, Format.NOT_SPECIFIED,
				Format.TRUE,Format.NOT_SPECIFIED)};
        private Format []outputFormats = new Format[]{new RGBFormat(null,Format.NOT_SPECIFIED,Format.byteArray,
				Format.NOT_SPECIFIED,24,3, 2, 1,3, Format.NOT_SPECIFIED,
				Format.TRUE,Format.NOT_SPECIFIED)};
        
        
    }
    
    private ReflectionMediaData reflection = null;
    volatile boolean useblackwhite = false, closeDataSource = true;
    PostAccessCodec postAccessCodec = null;
    MediaBounds mediaBounds = MediaBounds.LEFT;
    private Processor processor = null;
    private Object waitSync = new Object();
    private boolean stateTransitionOK = true;
}
